Passed
Push — master ( 9c80d9...628b52 )
by MusikAnimal
05:31
created

window.loadContributions   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
nc 4
nop 1
dl 0
loc 12
rs 9.4285
c 0
b 0
f 0
1
(function () {
2
    var $tocClone, tocHeight, sectionOffset = {}, apiPath, lastProject, editOffset;
3
4
    /** global: i18nLang */
5
    /** global: i18nPaths */
6
    $.i18n({
7
        locale: i18nLang
8
    }).load(i18nPaths);
9
10
    $(document).ready(function () {
11
        // TODO: move these listeners to a setup function and document how to use it.
12
        $('.xt-hide').on('click', function () {
13
            $(this).hide();
14
            $(this).siblings('.xt-show').show();
15
16
            if ($(this).parents('.panel-heading').length) {
17
                $(this).parents('.panel-heading').siblings('.panel-body').hide();
18
            } else {
19
                $(this).parents('.xt-show-hide--parent').next('.xt-show-hide--target').hide();
20
            }
21
        });
22
        $('.xt-show').on('click', function () {
23
            $(this).hide();
24
            $(this).siblings('.xt-hide').show();
25
26
            if ($(this).parents('.panel-heading').length) {
27
                $(this).parents('.panel-heading').siblings('.panel-body').show();
28
            } else {
29
                $(this).parents('.xt-show-hide--parent').next('.xt-show-hide--target').show();
30
            }
31
        });
32
33
        setupNavCollapsing();
34
        setupColumnSorting();
35
        setupTOC();
36
        setupStickyHeader();
37
        setupProjectListener();
38
        setupAutocompletion();
39
        displayWaitingNoticeOnSubmission();
40
41
        // Re-init forms, workaround for issues with Safari and Firefox.
42
        // See displayWaitingNoticeOnSubmission() for more.
43
        window.onpageshow = function (e) {
44
            if (e.persisted) {
45
                displayWaitingNoticeOnSubmission(true);
46
            }
47
        };
48
    });
49
50
    /**
51
     * Script to make interactive toggle table and pie chart.
52
     * For visual example, see the "Semi-automated edits" section of the AutoEdits tool.
53
     *
54
     * Example usage (see autoEdits/result.html.twig and js/autoedits.js for more):
55
     *     <table class="table table-bordered table-hover table-striped toggle-table">
56
     *         <thead>...</thead>
57
     *         <tbody>
58
     *             {% for tool, values in semi_automated %}
59
     *             <tr>
60
     *                 <!-- use the 'linked' class here because the cell contains a link -->
61
     *                 <td class="sort-entry--tool linked" data-value="{{ tool }}">
62
     *                     <span class="toggle-table--toggle" data-index="{{ loop.index0 }}" data-key="{{ tool }}">
63
     *                         <span class="glyphicon glyphicon-remove"></span>
64
     *                         <span class="color-icon" style="background:{{ chartColor(loop.index0) }}"></span>
65
     *                     </span>
66
     *                     {{ wiki.pageLink(...) }}
67
     *                 </td>
68
     *                 <td class="sort-entry--count" data-value="{{ values.count }}">
69
     *                     {{ values.count }}
70
     *                 </td>
71
     *             </tr>
72
     *             {% endfor %}
73
     *             ...
74
     *         </tbody>
75
     *     </table>
76
     *     <div class="toggle-table--chart">
77
     *         <canvas id="tool_chart" width="400" height="400"></canvas>
78
     *     </div>
79
     *     <script>
80
     *         window.toolsChart = new Chart($('#tool_chart'), { ... });
81
     *         window.countsByTool = {{ semi_automated | json_encode() | raw }};
82
     *         ...
83
     *
84
     *         // See autoedits.js for more
85
     *         window.setupToggleTable(window.countsByTool, window.toolsChart, 'count', function (newData) {
86
     *             // update the totals in toggle table based on newData
87
     *         });
88
     *     </script>
89
     *
90
     * @param  {Object}      dataSource  Object of data that makes up the chart
91
     * @param  {Chart}       chartObj    Reference to the pie chart associated with the .toggle-table
92
     * @param  {String|null} [valueKey]  The name of the key within entries of dataSource, where the value is
93
     *                                   what's shown in the chart. If omitted or null, `dataSource` is assumed
94
     *                                   to be of the structure: { 'a' => 123, 'b' => 456 }
95
     * @param  {Function} updateCallback Callback to update the .toggle-table totals. `toggleTableData`
96
     *                                   is passed in which contains the new data, you just need to
97
     *                                   format it (maybe need to use i18n, update multiple cells, etc.).
98
     *                                   The second parameter that is passed back is the 'key' of the toggled
99
     *                                   item, and the third is the index of the item.
100
     */
101
    window.setupToggleTable = function (dataSource, chartObj, valueKey, updateCallback) {
102
        var toggleTableData;
103
104
        $('.toggle-table').on('click', '.toggle-table--toggle', function () {
105
            if (!toggleTableData) {
106
                // must be cloned
107
                toggleTableData = Object.assign({}, dataSource);
108
            }
109
110
            var index = $(this).data('index'),
111
                key = $(this).data('key');
112
113
            // must use .attr instead of .prop as sorting script will clone DOM elements
114
            if ($(this).attr('data-disabled') === 'true') {
115
                toggleTableData[key] = dataSource[key];
116
                var oldValue = (
117
                    parseInt(valueKey ? toggleTableData[key][valueKey] : toggleTableData[key], 10)
118
                );
119
                chartObj.data.datasets[0].data[index] = oldValue;
120
                $(this).attr('data-disabled', 'false');
121
            } else {
122
                delete toggleTableData[key];
123
                chartObj.data.datasets[0].data[index] = null;
124
                $(this).attr('data-disabled', 'true');
125
            }
126
127
            // gray out row in table
128
            $(this).parents('tr').toggleClass('excluded');
129
130
            // change the hover icon from a 'x' to a '+'
131
            $(this).find('.glyphicon').toggleClass('glyphicon-remove').toggleClass('glyphicon-plus');
132
133
            // update stats
134
            updateCallback(toggleTableData, key, index);
135
136
            chartObj.update();
137
        });
138
    }
139
140
    /**
141
     * If there are more tool links in the nav than will fit in the viewport,
142
     *   move the last entry to the More menu, one at a time, until it all fits.
143
     * This does not listen for window resize events.
144
     */
145
    function setupNavCollapsing()
146
    {
147
        var windowWidth = $(window).width(),
148
            toolNavWidth = $('.tool-links').outerWidth(),
149
            navRightWidth = $('.nav-buttons').outerWidth();
150
151
        // Ignore if in mobile responsive view
152
        if (windowWidth < 768) {
153
            return;
154
        }
155
156
        // Do this first so we account for the space the More menu takes up
157
        if (toolNavWidth + navRightWidth > windowWidth) {
158
            $('.tool-links--more').removeClass('hidden');
159
        }
160
161
        // Don't loop more than there are links in the nav.
162
        // This more just a safeguard against an infinite loop should something go wrong.
163
        var numLinks = $('.tool-links--entry').length;
164
        while (numLinks > 0 && toolNavWidth + navRightWidth > windowWidth) {
165
            // Remove the last tool link that is not the current tool being used
166
            var $link = $('.tool-links--nav > .tool-links--entry:not(.active)').last().remove();
167
            $('.tool-links--more .dropdown-menu').append($link);
168
            toolNavWidth = $('.tool-links').outerWidth();
169
            numLinks--;
170
        }
171
    }
172
173
    /**
174
     * Sorting of columns
175
     *
176
     *  Example usage:
177
     *   {% for key in ['username', 'edits', 'minor', 'date'] %}
178
     *      <th>
179
     *         <span class="sort-link sort-link--{{ key }}" data-column="{{ key }}">
180
     *            {{ msg(key) | capitalize }}
181
     *            <span class="glyphicon glyphicon-sort"></span>
182
     *         </span>
183
     *      </th>
184
     *  {% endfor %}
185
     *   <th class="sort-link" data-column="username">Username</th>
186
     *   ...
187
     *   <td class="sort-entry--username" data-value="{{ username }}">{{ username }}</td>
188
     *   ...
189
     *
190
     * Data type is automatically determined, with support for integer,
191
     *   floats, and strings, including date strings (e.g. "2016-01-01 12:59")
192
     */
193
    window.setupColumnSorting = function () {
194
        var sortDirection, sortColumn;
195
196
        $('.sort-link').on('click', function () {
197
            sortDirection = sortColumn === $(this).data('column') ? -sortDirection : 1;
198
199
            $('.sort-link .glyphicon').removeClass('glyphicon-sort-by-alphabet-alt glyphicon-sort-by-alphabet').addClass('glyphicon-sort');
200
            var newSortClassName = sortDirection === 1 ? 'glyphicon-sort-by-alphabet-alt' : 'glyphicon-sort-by-alphabet';
201
            $(this).find('.glyphicon').addClass(newSortClassName).removeClass('glyphicon-sort');
202
203
            sortColumn = $(this).data('column');
204
            var $table = $(this).parents('table');
205
            var $entries = $table.find('.sort-entry--' + sortColumn).parent();
206
207
            if (!$entries.length) {
208
                return;
209
            }
210
211
            $entries.sort(function (a, b) {
212
                var before = $(a).find('.sort-entry--' + sortColumn).data('value'),
213
                    after = $(b).find('.sort-entry--' + sortColumn).data('value');
214
215
                // test data type, assumed to be string if can't be parsed as float
216
                if (!isNaN(parseFloat(before, 10))) {
217
                    before = parseFloat(before, 10);
218
                    after = parseFloat(after, 10);
219
                }
220
221
                if (before < after) {
222
                    return sortDirection;
223
                } else if (before > after) {
224
                    return -sortDirection;
225
                } else {
226
                    return 0;
227
                }
228
            });
229
230
            // Re-fill the rank column, if applicable.
231
            if ($('.sort-entry--rank').length > 0) {
232
                $.each($entries, function (index, entry) {
233
                    $(entry).find('.sort-entry--rank').text(index + 1);
234
                });
235
            }
236
237
            $table.find('tbody').html($entries);
238
        });
239
    }
240
241
    /**
242
     * Floating table of contents
243
     *
244
     * Example usage (see articleInfo/result.html.twig for more):
245
     *     <p class="text-center xt-heading-subtitle">
246
     *         ...
247
     *     </p>
248
     *     <div class="text-center xt-toc">
249
     *         {% set sections = ['generalstats', 'usertable', 'yearcounts', 'monthcounts'] %}
250
     *         {% for section in sections %}
251
     *             <span>
252
     *                 <a href="#{{ section }}" data-section="{{ section }}">{{ msg(section) }}</a>
253
     *             </span>
254
     *         {% endfor %}
255
     *     </div>
256
     *     ...
257
     *     {% set content %}
258
     *         ...content for general stats...
259
     *     {% endset %}
260
     *     {{ layout.content_block('generalstats', content) }}
261
     *     ...
262
     */
263
    function setupTOC()
264
    {
265
        var $toc = $('.xt-toc');
266
267
        if (!$toc || !$toc[0]) {
268
            return;
269
        }
270
271
        tocHeight = $toc.height();
272
273
        // listeners on the section links
274
        var setupTocListeners = function () {
275
            $('.xt-toc').find('a').off('click').on('click', function (e) {
276
                document.activeElement.blur();
277
                var $newSection = $('#' + $(e.target).data('section'));
278
                $(window).scrollTop($newSection.offset().top - tocHeight);
279
280
                $(this).parents('.xt-toc').find('a').removeClass('bold');
281
282
                createTocClone();
283
                $tocClone.addClass('bold');
284
            });
285
        };
286
        window.setupTocListeners = setupTocListeners;
287
288
        // clone the TOC and add position:fixed
289
        var createTocClone = function () {
290
            if ($tocClone) {
291
                return;
292
            }
293
            $tocClone = $toc.clone();
294
            $tocClone.addClass('fixed');
295
            $toc.after($tocClone);
296
            setupTocListeners();
297
        };
298
299
        // build object containing offsets of each section
300
        window.buildSectionOffsets = function () {
301
            $.each($toc.find('a'), function (index, tocMember) {
302
                var id = $(tocMember).data('section');
303
                sectionOffset[id] = $('#' + id).offset().top;
304
            });
305
        }
306
307
        // rebuild section offsets when sections are shown/hidden
308
        $('.xt-show, .xt-hide').on('click', buildSectionOffsets);
0 ignored issues
show
Bug introduced by
The variable buildSectionOffsets seems to be never declared. If this is a global, consider adding a /** global: buildSectionOffsets */ comment.

This checks looks for references to variables that have not been declared. This is most likey a typographical error or a variable has been renamed.

To learn more about declaring variables in Javascript, see the MDN.

Loading history...
309
310
        buildSectionOffsets();
311
        setupTocListeners();
312
313
        var tocOffsetTop = $toc.offset().top;
314
        $(window).on('scroll.toc', function (e) {
315
            var windowOffset = $(e.target).scrollTop();
316
            var inRange = windowOffset > tocOffsetTop;
317
318
            if (inRange) {
319
                if (!$tocClone) {
320
                    createTocClone();
321
                }
322
323
                // bolden the link for whichever section we're in
324
                var $activeMember;
325
                Object.keys(sectionOffset).forEach(function (section) {
326
                    if (windowOffset > sectionOffset[section] - tocHeight - 1) {
327
                        $activeMember = $tocClone.find('a[data-section="' + section + '"]');
0 ignored issues
show
Bug introduced by
The variable $tocClone seems to not be initialized for all possible execution paths.
Loading history...
328
                    }
329
                });
330
                $tocClone.find('a').removeClass('bold');
331
                if ($activeMember) {
332
                    $activeMember.addClass('bold');
333
                }
334
            } else if (!inRange && $tocClone) {
335
                // remove the clone once we're out of range
336
                $tocClone.remove();
337
                $tocClone = null;
338
            }
339
        });
340
    }
341
342
    /**
343
     * Make any tables with the class 'table-sticky-header' have sticky headers.
344
     * E.g. as you scroll the heading row will be fixed at the top for reference.
345
     */
346
    function setupStickyHeader()
347
    {
348
        var $header = $('.table-sticky-header');
349
350
        if (!$header || !$header[0]) {
351
            return;
352
        }
353
354
        var headerHeight = $header.height(),
0 ignored issues
show
Unused Code introduced by
The variable headerHeight seems to be never used. Consider removing it.
Loading history...
355
            $headerRow = $header.find('thead tr').eq(0),
356
            $headerClone;
357
358
        // Make a clone of the header to maintain placement of the original header,
359
        // making the original header the sticky one. This way event listeners on it
360
        // (such as column sorting) will still work.
361
        var cloneHeader = function () {
362
            if ($headerClone) {
363
                return;
364
            }
365
366
            $headerClone = $headerRow.clone();
367
            $headerRow.addClass('sticky-heading');
368
            $headerRow.before($headerClone);
369
370
            // Explicitly set widths of each column, which are lost with position:absolute.
371
            $headerRow.find('th').each(function (index) {
372
                $(this).css('width', $headerClone.find('th').eq(index).outerWidth());
373
            });
374
            $headerRow.css('width', $headerClone.outerWidth() + 1);
375
        };
376
377
        var headerOffsetTop = $header.offset().top;
378
        $(window).on('scroll.stickyHeader', function (e) {
379
            var windowOffset = $(e.target).scrollTop();
380
            var inRange = windowOffset > headerOffsetTop;
381
382
            if (inRange && !$headerClone) {
383
                cloneHeader();
384
            } else if (!inRange && $headerClone) {
385
                // Remove the clone once we're out of range,
386
                // and make the original un-sticky.
387
                $headerRow.removeClass('sticky-heading');
388
                $headerClone.remove();
389
                $headerClone = null;
390
            } else if ($headerClone) {
391
                // The header is position:absolute so it will follow with X scrolling,
392
                // but for Y we must go by the window scroll position.
393
                $headerRow.css(
394
                    'top',
395
                    $(window).scrollTop() - $header.offset().top
396
                );
397
            }
398
        });
399
    }
400
401
    /**
402
     * Add listener to the project input field to update any
403
     * namespace selectors and autocompletion fields.
404
     */
405
    function setupProjectListener()
406
    {
407
        // Stop here if there is no project field
408
        if (!$("#project_input")) {
409
            return;
410
        }
411
412
        // If applicable, setup namespace selector with real time updates when changing projects.
413
        // This will also set `apiPath` so that autocompletion will query the right wiki.
414
        if ($('#project_input').length && $('#namespace_select').length) {
415
            setupNamespaceSelector();
416
        // Otherwise, if there's a user or page input field, we still need to update `apiPath`
417
        // for the user input autocompletion when the project is changed.
418
        } else if ($('#user_input')[0] || $('#article_input')[0]) {
419
            // keep track of last valid project
420
            lastProject = $('#project_input').val();
421
422
            $('#project_input').on('change', function () {
423
                var newProject = this.value;
424
425
                // Show the spinner.
426
                $(this).addClass('show-loader');
427
428
                /** global: xtBaseUrl */
429
                $.get(xtBaseUrl + 'api/project/normalize/' + newProject).done(function (data) {
430
                    // Keep track of project API path for use in page title autocompletion
431
                    apiPath = data.api;
432
                    lastProject = newProject;
433
                    setupAutocompletion();
434
                }).fail(
435
                    revertToValidProject.bind(this, newProject)
0 ignored issues
show
Unused Code introduced by
The call to bind does not seem necessary since the function revertToValidProject declared on line 505 does not use this.
Loading history...
436
                ).always(function () {
437
                    $('#project_input').removeClass('show-loader');
438
                });
439
            });
440
        }
441
    }
442
443
    /**
444
     * Use the wiki input field to populate the namespace selector.
445
     * This also updates `apiPath` and calls setupAutocompletion()
446
     */
447
    function setupNamespaceSelector()
448
    {
449
        // keep track of last valid project
450
        lastProject = $('#project_input').val();
451
452
        $('#project_input').on('change', function () {
453
            // Disable the namespace selector and show a spinner while the data loads.
454
            $('#namespace_select').prop('disabled', true);
455
            $(this).addClass('show-loader');
456
457
            var newProject = this.value;
458
459
            /** global: xtBaseUrl */
460
            $.get(xtBaseUrl + 'api/project/namespaces/' + newProject).done(function (data) {
461
                // Clone the 'all' option (even if there isn't one),
462
                // and replace the current option list with this.
463
                var $allOption = $('#namespace_select option[value="all"]').eq(0).clone();
464
                $("#namespace_select").html($allOption);
465
466
                // Keep track of project API path for use in page title autocompletion
467
                apiPath = data.api;
468
469
                // Add all of the new namespace options.
470
                for (var ns in data.namespaces) {
471
                    if (!data.namespaces.hasOwnProperty(ns)) {
472
                        continue; // Skip keys from the prototype.
473
                    }
474
475
                    var nsName = parseInt(ns, 10) === 0 ? $.i18n('mainspace') : data.namespaces[ns];
476
                    $('#namespace_select').append(
477
                        "<option value=" + ns + ">" + nsName + "</option>"
478
                    );
479
                }
480
                // Default to mainspace being selected.
481
                $("#namespace_select").val(0);
482
                lastProject = newProject;
483
484
                // Re-init autocompletion
485
                setupAutocompletion();
486
            }).fail(revertToValidProject.bind(this, newProject)).always(function () {
0 ignored issues
show
Unused Code introduced by
The call to bind does not seem necessary since the function revertToValidProject declared on line 505 does not use this.
Loading history...
487
                $('#namespace_select').prop('disabled', false);
488
                $('#project_input').removeClass('show-loader');
489
            });
490
        });
491
492
        // If they change the namespace, update autocompletion,
493
        // which will ensure only pages in the selected namespace
494
        // show up in the autocompletion
495
        $('#namespace_select').on('change', setupAutocompletion);
496
    }
497
498
    /**
499
     * Called by setupNamespaceSelector or setupProjectListener
500
     *   when the user changes to a project that doesn't exist.
501
     * This throws a warning message and reverts back to the
502
     *   last valid project.
503
     * @param {string} newProject - project they attempted to add
504
     */
505
    function revertToValidProject(newProject)
506
    {
507
        $('#project_input').val(lastProject);
508
        $('.site-notice').append(
509
            "<div class='alert alert-warning alert-dismissible' role='alert'>" +
510
                $.i18n('invalid-project', "<strong>" + newProject + "</strong>") +
511
                "<button class='close' data-dismiss='alert' aria-label='Close'>" +
512
                    "<span aria-hidden='true'>&times;</span>" +
513
                "</button>" +
514
            "</div>"
515
        );
516
    }
517
518
    /**
519
     * Setup autocompletion of pages if a page input field is present.
520
     */
521
    function setupAutocompletion()
522
    {
523
        var $articleInput = $('#article_input'),
524
            $userInput = $('#user_input'),
525
            $namespaceInput = $("#namespace_select");
526
527
        // Make sure typeahead-compatible fields are present
528
        if (!$articleInput[0] && !$userInput[0] && !$('#project_input')[0]) {
529
            return;
530
        }
531
532
        // Destroy any existing instances
533
        if ($articleInput.data('typeahead')) {
534
            $articleInput.data('typeahead').destroy();
535
        }
536
        if ($userInput.data('typeahead')) {
537
            $userInput.data('typeahead').destroy();
538
        }
539
540
        // set initial value for the API url, which is put as a data attribute in forms.html.twig
541
        if (!apiPath) {
542
            apiPath = $('#article_input').data('api') || $('#user_input').data('api');
543
        }
544
545
        // Defaults for typeahead options. preDispatch and preProcess will be
546
        // set accordingly for each typeahead instance
547
        var typeaheadOpts = {
548
            url: apiPath,
549
            timeout: 200,
550
            triggerLength: 1,
551
            method: 'get',
552
            loadingClass: 'show-loader',
553
            preDispatch: null,
554
            preProcess: null,
555
        };
556
557
        if ($articleInput[0]) {
558
            $articleInput.typeahead({
559
                ajax: Object.assign(typeaheadOpts, {
560
                    preDispatch: function (query) {
561
                        // If there is a namespace selector, make sure we search
562
                        // only within that namespace
563
                        if ($namespaceInput[0] && $namespaceInput.val() !== '0') {
564
                            var nsName = $namespaceInput.find('option:selected').text().trim();
565
                            query = nsName + ':' + query;
566
                        }
567
                        return {
568
                            action: 'query',
569
                            list: 'prefixsearch',
570
                            format: 'json',
571
                            pssearch: query
572
                        };
573
                    },
574
                    preProcess: function (data) {
575
                        var nsName = '';
576
                        // Strip out namespace name if applicable
577
                        if ($namespaceInput[0] && $namespaceInput.val() !== '0') {
578
                            nsName = $namespaceInput.find('option:selected').text().trim();
579
                        }
580
                        return data.query.prefixsearch.map(function (elem) {
581
                            return elem.title.replace(new RegExp('^' + nsName + ':'), '');
582
                        });
583
                    },
584
                })
585
            });
586
        }
587
588
        if ($userInput[0]) {
589
            $userInput.typeahead({
590
                ajax: Object.assign(typeaheadOpts, {
591
                    preDispatch: function (query) {
592
                        return {
593
                            action: 'query',
594
                            list: 'prefixsearch',
595
                            format: 'json',
596
                            pssearch: 'User:' + query
597
                        };
598
                    },
599
                    preProcess: function (data) {
600
                        var results = data.query.prefixsearch.map(function (elem) {
601
                            return elem.title.split('/')[0].substr(elem.title.indexOf(':') + 1);
602
                        });
603
604
                        return results.filter(function (value, index, array) {
605
                            return array.indexOf(value) === index;
606
                        });
607
                    },
608
                })
609
            });
610
        }
611
    }
612
613
    /**
614
     * For any form submission, this disables the submit button and replaces its text with
615
     * a loading message and a counting timer.
616
     * @param {boolean} [undo] Revert the form back to the initial state.
617
     *                         This is used on page load to solve an issue with Safari and Firefox
618
     *                         where after browsing back to the form, the "loading" state persists.
619
     */
620
    function displayWaitingNoticeOnSubmission(undo)
621
    {
622
        if (undo) {
623
            // Re-enable form
624
            $('.form-control').prop('readonly', false);
625
            $('.form-submit').prop('disabled', false);
626
            $('.form-submit').text($.i18n('submit')).prop('disabled', false);
627
        } else {
628
            $('#content form').on('submit', function () {
629
                // Remove focus from any active element
630
                document.activeElement.blur();
631
632
                // Disable the form so they can't hit Enter to re-submit
633
                $('.form-control').prop('readonly', true);
634
635
                // Change the submit button text.
636
                $('.form-submit').prop('disabled', true)
637
                    .html($.i18n('loading') + " <span id='submit_timer'></span>");
638
639
                // Add the counter.
640
                var startTime = Date.now();
641
                setInterval(function () {
642
                    var elapsedSeconds = Math.round((Date.now() - startTime) / 1000);
643
                    var minutes = Math.floor(elapsedSeconds / 60);
644
                    var seconds = ('00' + (elapsedSeconds - (minutes * 60))).slice(-2);
645
                    $('#submit_timer').text(minutes + ":" + seconds);
646
                }, 1000);
647
            });
648
        }
649
    }
650
651
    /**
652
     * Set the initial offset for contributions lists, based on what was
653
     * supplied in the contributions container.
654
     */
655
    function setInitialEditOffset()
656
    {
657
        if (editOffset === undefined) {
658
            editOffset = parseInt($('.contributions-container').data('offset'), 10);
659
        }
660
    }
661
662
    /**
663
     * Loads configured type of contributions from the server and lists them in the DOM.
664
     * The navigation aids and showing/hiding of loading text is also handled here.
665
     */
666
    window.loadContributions = function (endpointFunc, apiTitle) {
667
        setInitialEditOffset();
668
669
        $('.contributions-loading').show();
670
        $('.contributions-container').hide();
671
672
        var params = $('.contributions-container').data(),
673
            endpoint = endpointFunc(params);
674
675
        /** global: xtBaseUrl */
676
        $.ajax({
677
            url: xtBaseUrl + endpoint + '/' + editOffset + '?htmlonly=yes',
678
            timeout: 30000
679
        }).done(function (data) {
680
            $('.contributions-container').html(data).show();
681
            $('.contributions-loading').hide();
682
            setupContributionsNavListeners(endpointFunc, apiTitle);
683
684
            if (editOffset > 0) {
685
                $('.contributions--prev').show();
686
            }
687
            if ($('.contributions-table tbody tr').length < 50) {
688
                $('.next-edits').hide();
689
            }
690
        }).fail(function (_xhr, _status, message) {
691
            $('.contributions-loading').hide();
692
            $('.contributions-container').html(
693
                $.i18n('api-error', apiTitle + ' API: <code>' + message + '</code>')
694
            ).show();
695
        });
696
    }
697
698
    /**
699
     * Set up listeners for navigating contribution lists.
700
     */
701
    window.setupContributionsNavListeners = function (endpointFunc, apiTitle) {
702
        setInitialEditOffset();
703
704
        $('.contributions--prev').one('click', function (e) {
705
            e.preventDefault();
706
            editOffset -= 50;
707
            loadContributions(endpointFunc, apiTitle)
708
        });
709
710
        $('.contributions--next').one('click', function (e) {
711
            e.preventDefault();
712
            editOffset += 50;
713
            loadContributions(endpointFunc, apiTitle);
714
        });
715
    }
716
})();
717